<template>
  <view class="container">
    <block v-for="(strObj, index) in dataArray" :key="index">
      <view
        v-if="strObj.show"
        class="line"
        :style="`padding-left: ${strObj.indent}rpx; font-size: ${props.fontSize}rpx;`"
      >
        <text
          v-if="collapsable && strObj.hasChild"
          :data-index="index"
          @longpress="expandAll"
          @tap="doExpand"
          :class="[clsgrp[clstype][strObj.expand]]"
        ></text>
        <view style="display: flex; flex-flow: row">
          <text :class="[strObj.ktype, kidx === index ? 'active' : '', 'key']" @tap="selectk(index)">
            {{ strObj.key }}
          </text>
          <text v-if="strObj.comma">{{ `${strObj.comma}` }}</text>
          <text v-if="strObj.value" :class="[strObj.type, vidx === index ? 'active' : '', 'val']" @tap="selectv(index)">
            {{ strObj.value }}
          </text>
          <text v-if="strObj.tail" class="val">{{ strObj.tail }}</text>
          <block v-if="!!strObj.suffix">
            <text class="val">{{ '...' }}</text>
            <text :class="[strObj.ktype || strObj.type, 'val']">{{ strObj.suffix }}</text>
          </block>
        </view>
      </view>
    </block>
  </view>
</template>

<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  obj: {
    // 添加容错，obj可传值为json数据，json字符串，普通数字，普通字符串
    type: [Object, String, Number],
    default() {
      return {}
    }
  },
  collapsable: {
    type: Boolean,
    default: true
  },
  fontSize: {
    type: Number,
    default: 24
  },
  icon: {
    type: String,
    default: 'arrow'
  }
})
const clstype = ref(['arrow', 'plusminus'].includes(props.icon) ? props.icon : 'plusminus')
const clsgrp = ref({
  arrow: {
    true: 'arrow-down',
    false: 'arrow-right'
  },
  plusminus: {
    true: 'minus',
    false: 'plus'
  }
})
const dataArray = ref([])
const basecode = ref('0123456789abcdefghijklmnopqrstuvwxyz')

/**
 * 判断是否是json字符串
 * @param {Object} str 要检查的字符串
 */
function isJsonString(str) {
  try {
    JSON.parse(str)
  } catch (e) {
    return false
  }
  return true
}
const dataTransform = () => {
  // let str = JSON.stringify(props.obj, null, ' ')
  // 容错判断，obj可传值为json数据，json字符串，普通数字，普通字符串
  let josnstr = isJsonString(props.obj) ? JSON.parse(props.obj) : props.obj
  let str = JSON.stringify(josnstr, null, ' ')
  let pl = 0
  let idn = 0
  // 提取 JSON 字符串中的键值对，其中键是双引号括起来的字符串，值可以是双引号括起来的字符串或其他类型的值
  let reg = /"(.+)"\s*:\s*("?.+"?),?/
  let arr = str
    .split(/\n/g)
    .reverse()
    .map((t) => {
      let res = {
        text: t
      }
      let regRes = reg.exec(t)
      if (regRes) {
        res.key = regRes[1].trim()
        res.value = regRes[2].replace(/,$/, '')
        res.tail = regRes[2] == res.value ? '' : ','
        res.comma = ':'
        if ('{' === res.value) {
          res.type = 'Object'
        } else if ('[' === res.value) {
          res.type = 'Array'
        } else {
          try {
            // 如果存在第二层双引号则字符串
            if (/".*?"/.test(res.value)) {
              res['type'] = 'String'
            } else {
              res['type'] = 'Number'
            }
          } catch (e) {
            console.warn(e)
          }
        }
      } else {
        res.key = t
        res.value = ''
        res.comma = ''
        if (!['{', '}', '[', ']'].includes(t.trim().replace(/,$/, '').replace(/\"/g, ''))) {
          try {
            // 如果存在第二层双引号则字符串
            if (/".*?"/.test(t)) {
              res['ktype'] = 'String'
            } else {
              res['ktype'] = 'Number'
            }
          } catch (e) {
            console.warn(e)
          }
        } else {
          if (t.indexOf(']') > -1 || t.indexOf('[') > -1) {
            res['ktype'] = 'Array'
          } else {
            res['ktype'] = 'Object'
          }
        }
      }
      res.indent = t.search(/[^\s]/) * props.fontSize
      res.hasChild = res.indent < pl
      res.expand = true
      res.show = true
      res.suffix = ''
      pl = res.indent
      return res
    })
  pl = -1
  let pcode = ''

  dataArray.value = arr.reverse().map((item) => {
    const cl = item.indent / props.fontSize
    if (cl > pl) {
      item.treeCode = pcode + basecode.value.charAt(0)
    } else if (cl == pl) {
      var lastIdx = basecode.value.indexOf(pcode.charAt(pcode.length - 1)) + 1
      item.treeCode = pcode.substr(0, pcode.length - 1) + basecode.value.charAt(lastIdx)
    } else {
      item.treeCode = pcode.substr(0, pcode.length - 1)
    }
    pl = cl
    pcode = item.treeCode
    return item
  })
}

dataTransform()
const expandAll = (e) => {
  let index = 0
  if (e) {
    const { index: k } = e.currentTarget.dataset
    index = parseInt(k)
  }
  const code = dataArray.value[index].treeCode
  dataArray.value = dataArray.value.map((item) => {
    if (item.treeCode.startsWith(code)) {
      item.suffix = ''
      item.show = true
      item.expand = true
    }
    return item
  })
}
const getExpandStatus = (code) => {
  return dataArray.value.find((item) => item.treeCode == code).expand
}
const isTail = (idx) => {
  let code = dataArray.value[idx].treeCode
  let index = dataArray.value.findIndex((item) => item.treeCode == code)
  return index < idx
}
const getParentsExpandStatus = (code) => {
  if (code == '0') return true
  let parents = dataArray.value.filter((item) => code.startsWith(item.treeCode) && item.treeCode != code)
  let res = true
  parents.forEach((item) => {
    if (!item.expand) {
      res = false
    }
  })
  return res
}
const print = () => {
  console.log(props.obj)
  console.log(JSON.stringify(props.obj))
}
const doExpand = (e) => {
  let index = 0
  if (e) {
    const { index: k } = e.currentTarget.dataset
    index = parseInt(k)
  }
  let suffixText = ''
  const code = dataArray.value[index].treeCode
  const expand = !dataArray.value[index].expand //点击之后的展开状态
  dataArray.value[index].expand = expand //更新展开状态
  const arr = dataArray.value.map((item, idx) => {
    if (item.treeCode.startsWith(code) && idx != index) {
      //排除本身
      if (!expand) {
        //如果折叠，子项全部隐藏
        item.show = false
      } else {
        //如果展开，子项的尾巴是否显示看他的父级展开状态
        item.show = getParentsExpandStatus(item.treeCode)
      }
      if (item.treeCode == code) {
        // last one
        suffixText = expand ? '' : `${item.text.trim()}`
      }
      // 隐藏结尾的 ] } 等
      if (isTail(idx) && !getExpandStatus(item.treeCode)) {
        item.show = false
      }
    }
    // 处理后缀
    if (item.expand) {
      item.suffix = ''
    }
    return item
  })
  dataArray.value[index].suffix = suffixText
  dataArray.value = arr
}

const vidx = ref(-1)
const kidx = ref(-1)
let vt = 0,
  kt = 0
const selectk = (idx) => {
  vidx.value = idx
  clearTimeout(vt)
  vt = setTimeout(() => {
    vidx.value = -1
  }, 600)
  if (props.collapsable)
    doExpand({
      currentTarget: {
        dataset: {
          index: idx
        }
      }
    })
}
const selectv = (idx) => {
  kidx.value = idx
  clearTimeout(kt)
  kt = setTimeout(() => {
    kidx.value = -1
  }, 600)
}
watch(
  () => props.collapsable,
  (n, o) => {
    if (!n) {
      expandAll()
    }
  }
)
watch(
  () => props.icon,
  (n, o) => {
    if (!!n) {
      clstype.value = ['arrow', 'plusminus'].includes(n) ? n : 'plusminus'
    }
  }
)
defineExpose({
  print,
  doExpand
})
</script>

<style scoped>
.container {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  align-content: center;
  justify-content: flex-start;
  box-sizing: border-box;
}

.container .line {
  display: flex;
  position: relative;
  align-items: center;
  justify-content: flex-start;
  width: inherit;
  box-sizing: border-box;
}

text {
  margin: 0 4rpx;
}

.key,
.val {
  height: min-content;
  display: inline-block;
  transition-duration: 0.3s;
  border-bottom: solid 4rpx transparent;
  font-weight: bold;
}

.key.active,
.val.active {
  transition-duration: 0.3s;
  border-bottom: solid 4rpx rgba(255, 90, 20, 0.8);
}

.key {
  color: #95afc0;
}

.val {
  color: #444;
}

.String {
  color: #718093;
}

.Number {
  color: #686de0;
}

.Array {
  color: #8c7ae6;
}

.Object {
  color: #4cd137;
}

.expand {
  display: inline-flex;
  width: 32rpx;
  height: 32rpx;
  padding: 0;
  align-items: center;
  transform: scale(0.8);
  justify-items: center;
  justify-content: center;
  border: solid 1rpx lightgray;
  box-sizing: border-box;
  color: gray;
  font-size: 24rpx;
}

.arrow-right,
.arrow-down {
  display: inline-block;
  width: 40rpx;
  height: 40rpx;
  background-image: url('');
  background-size: cover;
  transform: rotate(-90deg);
  -ms-transform: rotate(-90deg);
  -moz-transform: rotate(-90deg);
  -webkit-transform: rotate(-90deg);
  -o-transform: rotate(-90deg);
}

.arrow-down {
  transform: rotate(0deg);
  -ms-transform: rotate(0deg);
  -moz-transform: rotate(0deg);
  -webkit-transform: rotate(0deg);
  -o-transform: rotate(0deg);
}

.minus,
.plus {
  display: inline-block;
  width: 40rpx;
  height: 40rpx;
  background-size: cover;
}

.plus {
  background-image: url('');
}

.minus {
  background-image: url('');
}
</style>
